<?php
namespace TbfHttp;
use Guzzle\Common\Exception\InvalidArgumentException;
use Guzzle\Http\Client;
use \Guzzle\Http\Exception\BadResponseException;
use Guzzle\Http\ClientInterface;
use Guzzle\Http\RedirectPlugin;
use Guzzle\Http\Url;

/**
 * 发送ajax请求,模拟浏览器上的jquery接口
 * 构造方法
 $this['client'] = $this->share(function($c){
     $client = new \Guzzle\Service\Client($c['host'],$c['client_config']);
     $client->addSubscriber($c['cookie_plugin']);

     return $client;
        });
        $this['ajax'] = $this->share(function($c){
            $ajax = new \Tbf\Http\Ajax($c['client']);
            if (!empty($c['test_code'])) {
                $ajax->setTestCode($c['test_code']);
            }

            return $ajax;
        });
    使用方法
        $ajax(array(
            'data'=>array(), //数据
            'method'=>'GET', //http方法
            'url'=>null,     //url
            'debug'=>false,  //是否显示调试信息
            'throw'=>true,   //是否抛出异常
            'return'=>'json' //(希望的)返回类型
            'cookie'=>null,  //cookie
            'header'=>null,  //添加header
            'test'=>true,    //是否告诉服务器端,这个请求是test
            'redirect'=>false, //是否跟随跳转?
        ))
        返回类型有:
        'json' //返回json对象
        'string' //返回字符串
        'response'  //直接返回guzzle的response对象
        * */
class Ajax
{
    /** @var \Guzzle\Http\Client */
    public $client = null;
    /** @var \Guzzle\Http\Message\Request  */
    protected $request = null;
    /** @var  string */
    protected $origin_request;
    /** @var \Guzzle\Http\Message\Response  */
    protected $response = null;
    public $testCode = null;
    /** @var \Guzzle\Plugin\Cookie\CookiePlugin */
    public $cookie_plugin = null;
    public $runTimes = 0;
    protected $ajax_req = array();

    public function __construct(ClientInterface $client)
    {
        $this->client = $client;
        $this->reset();
        $dispatcher = $this->client->getEventDispatcher();
        $dispatcher->addListener('request.before_send',array($this,'handleCookie'));
    }

    public function __invoke($origin_data)
    {
        return $this->request($origin_data);
    }

    public function setTestCode($testCode)
    {
        $this->testCode = $testCode;
    }

    /**
     * 处理请求
     * 根据需要确定是否抛出异常
     * @param $ajax_req
     * @throws \Exception
     */
    public function request($ajax_req)
    {
        $default = array(
            'data'=>array(), //数据
            'method'=>'GET', //http方法 GET, POST
            'url'=>null,     //url
            'debug'=>false,  //是否显示调试信息
            'throw'=>true,   //是否抛出异常
            'input_type'=>'http_query', //输入的数据类型 http_query json
            'return'=>'json',//(希望的)返回类型 json string ignore
            'ajax'=>true,    //是否告诉服务器端,这个请求是ajax
            'header'=>null,  //添加header,需要传入一个数组,会覆盖原有的header
            'cookie'=>null,  //添加cookie
            'test'=>true,    //是否告诉服务器端,这个请求是test
            'redirect'=>false, //是否跟随跳转?
            'origin_header'=>true,  //是否添加origin头(绕过csrf防御机制)
        );
        $this->ajax_req = array_merge($default,$ajax_req);
        foreach ($this->ajax_req['data'] as $k1=>$v1) {
            if ($v1===null) {
                unset($this->ajax_req['data'][$k1]);
            }
        }
        $data = $this->rawRequest();

        return $data;
    }
    /**
     * 处理请求
     * 会抛出异常
     * */
    protected function rawRequest()
    {
        $client = $this->client;
        $client->setUriTemplate(new UriTemplateStub);
        $this->ajax_req['method'] = strtoupper($this->ajax_req['method']);

        $this->request = $this->loadRequestData($client);

        if (!$this->ajax_req['redirect']) {
            $this->request->getParams()->set(RedirectPlugin::DISABLE, true);
        }

        //workaround a bug of guzzle
        $this->workAroundColon($this->ajax_req['url']);

        if (!empty($this->ajax_req['origin_header'])){
            $scheme = $this->request->getScheme();
            $port = $this->request->getPort();
            if (($scheme==='http' && $port===80) || ($scheme==='https' && $port===443) ) {
                $this->ajax_req['header']['Origin'] = $this->request->getScheme()."://".$this->request->getHost();
            }else{
                $this->ajax_req['header']['Origin'] = $this->request->getScheme()."://".$this->request->getHost().':'.$port;
            }
        }
        if (!empty($this->ajax_req['header'])) {
            $header = $this->ajax_req['header'];
            if (!is_array($header)){
                throw new \Exception('header is not array');
            }
            foreach ($header as $k=>$v) {
                $this->request->addHeader($k,$v);
            }
        }
        //告诉服务器这是ajax
        if (!empty($this->ajax_req['ajax'])) {
            $this->request->setHeader('X-Requested-With','XMLHttpRequest');
        }
        //告诉服务器这是测试
        if (($this->ajax_req['test'])&&(!empty($this->testCode))) {
            $this->request->setHeader('X-Test-Code',$this->testCode);
        }
        //在跟随跳转的过程中会导致request的字符串被改变.
        $this->origin_request = (string) $this->request;
        try {
            $this->response = $this->request->send();
        } catch (BadResponseException $e) {
            $this->request = $e->getRequest();
            $this->response = $e->getResponse();
            if ($this->ajax_req['throw']==true) {
                $this->showDebug();
                throw $e;
            }
        }
        $hasShowDebug = false;
        //调试
        if ($this->ajax_req['debug']) {
            $hasShowDebug = true;
            $this->showDebug();
        }
        switch ($this->ajax_req['return']){
            case 'response':
                return $this->response;
            case 'string':
                return $this->response->getBody(true);
            case 'ignore':
                return null;
            case 'json':
                $response_body = $this->response->getBody(true);
                $json_data = json_decode($response_body,true);
                if ($json_data===null&&$this->ajax_req['throw']) {
                    if (!$hasShowDebug) {
                        $this->showDebug();
                    }
                    throw new \Exception('not json data type');
                }
                return $json_data;
            default:
                throw new \Exception('unknow data return value:'.$this->ajax_req['return']);
        }
    }
    /**
     * 添加cookie
     * 绕过guzzle的cookie插件(在它后面产生)
     * 字符串 覆盖
     * 数组 添加
     * */
    public function handleCookie()
    {
        //cookie
        $cookie = $this->ajax_req['cookie'];
        if ($cookie!==null) {
            //覆盖原有cookie
            if (is_string($cookie)) {
                $this->request->addHeader('Cookie',$cookie);
            }
            //添加新cookie
            if (is_array($cookie)) {
                foreach ($cookie as $k1=>$v1) {
                    $this->request->addCookie($k1,$v1);
                }
            }
        }
    }
    /**
     * 清除本对象包含的所有状态
     * 清除cookie
     * */
    public function reset()
    {
        //重建cookie_plugin
        if (!empty($this->cookie_plugin)) {
            $this->client->getEventDispatcher()->removeSubscriber($this->cookie_plugin);
        }
        $this->cookie_plugin = new \Guzzle\Plugin\Cookie\CookiePlugin(
                new \Guzzle\Plugin\Cookie\CookieJar\ArrayCookieJar
        );
        $this->client->addSubscriber($this->cookie_plugin);
    }
    public function getCookie()
    {
        return $this->cookie_plugin->getCookieJar();
    }
    public function showDebug()
    {
        $new_request = (string) $this->request;
        if ($this->origin_request!=$new_request) {
            echo "\n-----origin request------------------------\n";
            echo $this->origin_request;
        }
        echo "\n-----request-------------------------------\n";
        echo $new_request;
        echo "\n-----response-------------------------------\n";
        echo (string) $this->response;
        echo "\n------------------------------------\n\n";

    }
    /**
     * 绕开guzzle的冒号bug
     * */
    public function workAroundColon($url)
    {
        if (strpos($url,':')===false) {
            return $url;
        }
        if (strpos($url,'://')!==false) {
            return $url;
        }
        $baseUrl = $this->client->getBaseUrl();
        $url = \TbfPath::urlJoin($baseUrl,$url);
        return $url;
    }

    protected function loadRequestData(Client $client)
    {
        //在url里面使用 ":" 问题(CakePhp的奇葩url..)
        $url_string = $this->ajax_req['url'];
        try{
            $url = Url::factory($url_string);
        }catch(InvalidArgumentException $e){
            $url = $this->workAroundColon($url_string);
        }

        //get方法传数据
        if ($this->ajax_req['method'] === 'GET') {
            if ($this->ajax_req['input_type']!=='http_query') {
                throw new \Exception('invalid input_type for GET request ,'.$this->ajax_req['input_type'] );
            }
            $request = $client->createRequest($this->ajax_req['method'],$url,null,null);
            if (empty($this->ajax_req['data'])) {
                return $request;
            }
            $url_obj = $request->getUrl(true);
            $old_data = $url_obj->getQuery();
            $old_data = $old_data->getAll();
            $new_data = array_merge($old_data, $this->ajax_req['data']);
            $url_obj->setQuery($new_data);
            $request->setUrl((string) $url_obj);

            return $request;
        }

        /** @var \Guzzle\Http\Message\EntityEnclosingRequest $request */
        if ($this->ajax_req['input_type'] === 'json') {
            $request = $client->createRequest($this->ajax_req['method'],$url,null,null);
            $input_body = json_encode($this->ajax_req['data']);
            $request->setBody($input_body, 'application/json');

            return $request;
        }
        if ($this->ajax_req['input_type'] === 'http_query') {
            $request = $client->createRequest($this->ajax_req['method'],$url,null,$this->ajax_req['data']);

            return $request;
        }
        throw new \Exception('invalid input_type:' . $this->ajax_req['input_type']);
    }

}
class UriTemplateStub implements \Guzzle\Parser\UriTemplate\UriTemplateInterface
{
    public function expand($a,array $b)
    {
        return $a;
    }
}
